In [1]:
%%HTML
<style>
.container { width:100% }
</style>


Utilities for Bitboards

The function board_2_bitmap takes two parameters:

  • board is a numpy array representing a board.
  • player is an integer from the set $\{1, -1\}$.

It returns an integers encoding the position of the player as a bitstring.

  48 49 50 51 52 53 54   55   
+----------------------+ 
| 40 41 42 43 44 45 46 | 47   top row
| 32 33 34 35 36 37 38 | 39
| 24 25 26 27 28 29 30 | 31
| 16 17 18 19 20 21 22 | 23
|  8  9 10 11 12 13 14 | 15 
|  0  1  2  3  4  5  6 |  7   bottom row
+----------------------+

In [2]:
top_line = (1 << 40) | (1 << 41) | (1 << 42) | (1 << 43) | (1 << 44) | (1 << 45) | (1 << 46)
top_line


Out[2]:
139637976727552

In [3]:
def bit_position(row, col):
    return row * 8 + col

In [4]:
def get_bit(bitboard, row, col):
    pos = bit_position(row, col)
    return (bitboard >> pos) & 1

In [5]:
def set_bit(bitboard, row, col):
    pos = bit_position(row, col)
    return bitboard | (1 << pos)

In [6]:
def board_2_bitmap(board, player):
    position = ''
    for row in range(5+1):
        for col in range(0, 6):
            if board[row, col] == player:
                position += '1'
            else:
                position += '0'
        position += '0'
    return int(position, 2)

Given a bitboard representing the pieces of one player, check whether there are four pieces in a horizontal, vertical, or diagonal line.


In [7]:
def has_four(bitboard):
    shifts = [1, 7, 8, 9]
    for s in shifts:
        if bitboard & (bitboard >> s) & (bitboard >> (2 * s)) & (bitboard >> (3 * s)):
            return True
    return False

Connect Four

This notebook defines the game Connect Four. You can play it online at: http://www.connectfour.org/connect-4-online.php.

Connect Four is played on a $6 \times 7$ board. Instead of Red and Yellow we call the players Xand O. Player X starts. Player X and O take turns to choose columns that are not yet filled. When player X chooses column c, the first non-empty field in column c is filled with an "X". Likewise, when player O chooses column c, the first non-empty field in column c is filled with an "X". Rows are numbered from the bottom up, i.e. the bottom row is row $0$. The goal of the game for player X is to get four consecutive Xs into a row, column, or diagonal line, while player O needs to get four consecutive Os into a row, column, or diagonal line.


In [8]:
Players = [0, 1]

States are represented as pairs of bitboards. Additionally, there is a 7 tuple showing how many pieces have been put into each column.


In [9]:
Start = (0, 0)
Start


Out[9]:
(0, 0)

The function find_empty takes two arguments:

  • State is a description of the board,
  • col specifies a column, i.e. it is an integer from the set $\{0, \cdots, 6\}$.

Given the State the function find_empty(State, col) returns the smallest $\texttt{row} \in \{0, \cdots, 5\}$ such that

    State[row][col] == ' '

holds. If the specified column is already completely filled, then instead None is returned.


In [10]:
def find_empty(state, col):
    b1, b2 = state
    board = b1 | b2
    for row in range(6):
        if not get_bit(board, row, col):
            return row
    return 6

Given a State and the player who has the next move, the function next_states(State, player) computes the set of states that can be reached from State.


In [11]:
def next_states(state, player):
    States = set()
    board  = state[player]
    for col in range(7):
        row = find_empty(state, col)
        if row != 6:
            new_board = set_bit(board, row, col)
            if player == 0:
                new_state = (new_board, state[1])
            else: 
                new_state = (state[0], new_board)
            States.add(new_state) 
    return States

The variable All_Lines collects the coordinates of all groups of four fields that are consecutive horizontally, vertically, or diagonally.


In [12]:
# horizontal lines
All_Lines  = [ [ (row, col+x) for x in range(3+1) ] for col in range(4)
                                                    for row in range(6) 
             ]
# vertical lines
All_Lines += [ [ (row+x, col) for x in range(4) ] for col in range(7)
                                                  for row in range(3) 
             ]
# rising diagonals
All_Lines += [ [ (row+x, col+x) for x in range(4) ] for col in range(4)
                                                    for row in range(3)
             ]
# falling diagonals
All_Lines += [ [ (row+x, col-x) for x in range(4) ] for col in range(3, 7)
                                                    for row in range(3)
             ]

Given a State the function top_line_filled(State) checks whether all marks in the top line of the given board are filled.


In [13]:
def top_line_filled(state):
    b1, b2 = state
    board  = b1 | b2
    return top_line & board == top_line

The function utility takes two arguments:

  • State is a tuple of tuple representing the board.
  • player is a player.

The function returns 1 if player has won the game, -1 if the game is lost for player, 0 if its a draw, and None if the game has not yet been decided.


In [14]:
def utility(state, player):
    b1, b2 = state
    if has_four(b1):
        return 1 - 2 * player
    if has_four(b2):
        return -1 + 2 * player
    # no winner so far, check for a draw
    if top_line_filled(state):  # no empty squares
        return 0
    return None

The function heuristic tries to guess the value of a state. As it is never called in terminal states, it assumes that the game will be drawn.


In [15]:
def get_mark(state, row, col):
    b1, b2 = state
    if get_bit(b1, row, col):
        return 'X'
    if get_bit(b2, row, col):
        return 'O'
    return ' '

In [16]:
def heuristic(state, player):
    b1, b2 = state
    result = 0.0
    # all lines are checked whether they contain either 3 or 2 identical nonempty marks 
    for Line in All_Lines:
        List = []
        for row, col in Line:
            mark = get_mark(state, row, col)
            if mark != ' ':
                List.append(mark)
        if len(List) == 3:
            Chars = set(List)
            if len(Chars) == 1:
                if Chars == { player }: 
                    result += 1/10
                else:
                    result -= 1/10
        if len(List) == 2:
            Chars = set(List)
            if len(Chars) == 1:
                if Chars == { player }: 
                    result += 1/100
                else:
                    result -= 1/100
    return result

finished(State) is True if the game is over.


In [17]:
def finished(state):
    return utility(state, 0) != None

The function get_move asks the user to input a move in the format r,c where r is the row and the c is the column where the next symbol is to be placed.


In [18]:
def get_move(state):
    b1, b2 = state
    while True:
        col = input("Enter column here: ")
        col = int(col)
        row = find_empty(state, col)
        if row != 6:
            b2 = set_bit(b2, row, col)
            return b1, b2
        else:
            print("Don't cheat.  Please try again.")

This function informs the user about the result of the game once the game is finished.


In [19]:
def final_msg(State):
    if finished(State):
        if utility(State, 1) == 1:
            print("You have won!")
        elif utility(State, 1) == -1:
            print("You have lost!")
        else:
            print("It's a draw.");
        return True
    return False

Drawing the Board


In [20]:
import ipycanvas as cnv

In [21]:
size = 50

This function creates the canvas for the start state. It draws an empty board which is later used for the game.


In [22]:
def create_canvas(Start):
    n = len(Start)
    canvas = cnv.Canvas(size=(size * 7, size * 8))
    display(canvas)
    return canvas

In [23]:
import math

The function draw takes three arguments:

  • State is the current state of the game.
  • canvas is a canvas used to draw the state.
  • value is the value of the game for player X.

The function draws the given State onto canvas. Below that, the value is printed.


In [24]:
def draw(state, canvas, value):
    b1, b2 = state
    canvas.clear()
    canvas.font = '36px sans-serif'
    canvas.text_align    = 'center'
    canvas.text_baseline = 'middle'
    for row in range(6):
        for col in range(7):
            x = col * size
            y = row * size
            canvas.line_width = 3.0
            canvas.stroke_rect(x, y, size, size)
            x += size // 2
            y += size // 2
            s1 = get_bit(b1, 5 - row, col)
            s2 = get_bit(b2, 5 - row, col)
            if s1 != 0:
                canvas.fill_style ='red'
                canvas.fill_arc(x, y, 0.4*size, 0, 2*math.pi)
            if s2 != 0:
                canvas.fill_style ='blue'
                canvas.fill_arc(x, y, 0.4*size, 0, 2*math.pi)
    canvas.font = '20px sans-serif'
    canvas.fill_style = 'black'
    for i in range(7):
        x = (i + 0.5) * size
        y = 6.4 * size
        canvas.fill_text(str(i), x, y) 
    x = 3.5 * size
    y = 7.4 * size
    canvas.fill_text(str(value), x, y)

In [ ]: